/*
Copyright 2021 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
	"errors"
	"flag"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"

	yaml "gopkg.in/yaml.v3"
)

var (
	configPath string
	outputPath string
	rootDir    string

	enabledModulesConfigPath string
)

type Config struct {
	ExcludeNamespaces    []string `yaml:"excludeNamespaces,omitempty"`
	AdditionalNamespaces []string `yaml:"additionalNamespaces,omitempty"`
}

type namespacesForIntegrity map[string]struct{}
type enabledModules map[string]struct{}

func (s enabledModules) enabled(module string) bool {
	if len(s) == 0 {
		return true
	}

	_, ok := s[module]
	return ok
}

func (o namespacesForIntegrity) String() string {
	list := make([]string, 0, len(o))
	for n := range o {
		list = append(list, n)
	}

	sort.Strings(list)

	outObj := struct {
		NamespacesForIntegrity []string `yaml:"namespaces_for_integrity"`
	}{
		NamespacesForIntegrity: list,
	}

	outBytes, err := yaml.Marshal(outObj)
	if err != nil {
		panic(fmt.Errorf("cannot marshal NamespacesForIntegrity: %v", err))
	}

	return "# Code generated by 'tools/generate_integrity_namespaces.go' DO NOT EDIT.\n" + string(outBytes)
}

func main() {
	initFlags()

	workDir := cwd()

	out, err := os.Create(outputPath)
	if err != nil {
		panic(fmt.Errorf("cannot create output: %v", err))
	}
	defer out.Close()

	config := parseConfig(configPath)

	var namespacesMap = make(namespacesForIntegrity)

	if len(config.ExcludeNamespaces) > 0 && config.ExcludeNamespaces[0] == "all" {
		logToStdErr("WARNING: All namespaces are excluded from integrity check.")
		writeNamespaces(out, namespacesMap)
		return
	}

	modulesForFilter := parseEnabledModulesConfig(enabledModulesConfigPath)

	res := findModules(workDir)

	for moduleYamlPath := range res {
		err = processModule(moduleYamlPath, namespacesMap, modulesForFilter)
		if err != nil {
			logToStdErr("error process moduleYamlPath: %s", moduleYamlPath)
			panic(err)
		}
	}

	for _, exclude := range config.ExcludeNamespaces {
		delete(namespacesMap, exclude)
	}

	for _, ns := range config.AdditionalNamespaces {
		namespacesMap[ns] = struct{}{}
	}

	writeNamespaces(out, namespacesMap)
}

func logToStdErr(f string, args ...any) {
	_, err := fmt.Fprintf(os.Stderr, f, args...)
	if err != nil {
		panic(err)
	}
}

func writeNamespaces(writer io.Writer, nss namespacesForIntegrity) {
	_, err := writer.Write([]byte(nss.String()))
	if err != nil {
		panic(fmt.Errorf("cannot write NamespacesForIntegrity: %v", err))
	}
}

func initFlags() {
	flag.StringVar(&configPath, "config", "", "Path to config for generator. Required")
	flag.StringVar(&outputPath, "output", "", "Path write result. Required")
	flag.StringVar(&rootDir, "root-dir", "", "Root directory for finding modules. If do not pass calculate from run")
	flag.StringVar(&enabledModulesConfigPath, "enabled-modules-config", "", "Path to yaml config map[string]bool for filter modules")

	flag.Parse()

	if configPath == "" {
		panic("-config flag is required")
	}

	if outputPath == "" {
		panic("-output flag is required")
	}
}

func parseEnabledModulesConfig(p string) enabledModules {
	res := make(enabledModules)
	if p == "" {
		return res
	}

	content, err := os.ReadFile(p)
	if err != nil {
		panic(fmt.Errorf("cannot read config: %v", err))
	}

	config := make(map[string]bool)

	if err := yaml.Unmarshal(content, &config); err != nil {
		panic(fmt.Errorf("cannot parse config: %v", err))
	}

	for module, enabled := range config {
		if enabled {
			res[module] = struct{}{}
		}
	}

	return res
}

func parseConfig(p string) Config {
	content, err := os.ReadFile(p)
	if err != nil {
		panic(fmt.Errorf("cannot read config: %v", err))
	}

	result := Config{}

	if err := yaml.Unmarshal(content, &result); err != nil {
		panic(fmt.Errorf("cannot parse config: %v", err))
	}

	return result
}

func cwd() string {
	var err error
	result := ""

	if rootDir != "" {
		result, err = filepath.Abs(rootDir)
		if err != nil {
			panic(err)
		}
	} else {
		_, f, _, ok := runtime.Caller(1)
		if !ok {
			panic("cannot get caller")
		}

		result, err = filepath.Abs(f)
		if err != nil {
			panic(err)
		}

		for i := 0; i < 3; i++ { // ../../
			result = filepath.Dir(result)
		}
	}

	// If deckhouse repo directory is symlinked (e.g. to /deckhouse), resolve the real path.
	// Otherwise, filepath.Walk will ignore all subdirectories.
	result, err = filepath.EvalSymlinks(result)
	if err != nil {
		panic(err)
	}

	return result
}

type moduleMetadata struct {
	Name      string `yaml:"name"`
	Namespace string `yaml:"namespace"`
}

func processModuleDefinition(path string) ( /*name*/ string /*namespace*/, string, error) {
	// if file exists
	var s moduleMetadata
	c, err := os.Open(path)
	if err != nil {
		return "", "", err
	}
	defer c.Close()

	err = yaml.NewDecoder(c).Decode(&s)
	if err != nil {
		return "", "", err
	}

	if s.Namespace == "" {
		// trim file name from path
		dir := filepath.Dir(path)
		ns, err := os.ReadFile(filepath.Join(dir, ".namespace"))
		if err != nil {
			if errors.Is(err, os.ErrNotExist) {
				return "", "", nil
			}
			return "", "", err
		}
		s.Namespace = strings.Trim(string(ns), "\r\n")
	}

	return s.Name, s.Namespace, nil
}

// findModules find all directories, contains module.yaml
// return map of absoulute path to module.yaml -> last directory, where module.yaml is located (xxx-module-name)
func findModules(root string) map[string]string {
	modulesMap := make(map[string]string)

	err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}

		if !info.IsDir() {
			return nil
		}

		modulePath := filepath.Join(path, "module.yaml")
		if _, err := os.Stat(modulePath); err != nil {
			return nil
		}

		relPath, err := filepath.Rel(root, path)
		if err != nil {
			return err
		}

		parts := strings.Split(filepath.ToSlash(relPath), "/")
		for _, part := range parts {
			if part == "modules" {
				dirName := filepath.Base(path)
				modulesMap[modulePath] = dirName
				break
			}
		}

		return nil
	})

	if err != nil {
		panic(fmt.Errorf("error walking the path %s: %v", root, err))
	}

	return modulesMap
}

func processModule(moduleYamlPath string, namespacesMap namespacesForIntegrity, modules enabledModules) error {
	name, namespace, err := processModuleDefinition(moduleYamlPath)
	if err != nil {
		return fmt.Errorf("error while processing modules")
	}

	if name == "" {
		return nil
	}

	if !modules.enabled(name) {
		return nil
	}

	if strings.HasPrefix(namespace, "d8-") || namespace == "kube-system" {
		namespacesMap[namespace] = struct{}{}
	}

	return nil
}
